package framework.world;

import framework.interfaces.RuleSet;
import framework.interfaces.Visualization;

/**
 * Manages the simulation framework.world that the cellular automaton exists in
 * Is configured using a RuleSet, and operates on a grid of CellImpl's
 */
public class World{

	//Do edges wrap around?
	private boolean toroidal;
	//The grid of cells
	private CellImpl[][] grid;
	
	//The plugins used to run and show the cells
	private RuleSet rules;
	private Visualization visualizer;
	
	//The size of the grid
	private int width;
	private int height;
	private static final int MAX_SIZE = 60; //Max dimension size of the grid
	
	//The global machine state for the simulation
	private int machineState;
	
	//Creates a simulation framework.world with the given RuleSet and Visualization plugin
	public World(RuleSet rules, Visualization vis){		
		this.rules = rules;
		this.visualizer = vis;
		
		//Reads RuleSet to configure framework.world
		machineState = rules.getStartingMachineState();
		toroidal = rules.getToroidal();
		height = rules.getGridRows();
		width = rules.getGridCols();
		if(height < 1 || width < 1){
			throw new IllegalArgumentException("World width and height can't be 0!");
		}
		if(height > MAX_SIZE){
			height = MAX_SIZE;
		}
		if(width > MAX_SIZE){
			width = MAX_SIZE;
		}
		//Create the grid of cells
		grid = new CellImpl[height][width];
		
		//If a starting grid is populated, fill it in
		int[][] startingGrid = rules.getStartingGrid();
		if(startingGrid != null && (startingGrid.length != height || 
									startingGrid[0].length != width)){
			throw new IllegalArgumentException("Starting grid doesn't match framework.world dimensions!");
		}

		//Finish applying rules for the framework.world
		CellImpl.NEIGHBORHOOD_SIZE = rules.getNeighborhoodSize();
		CellImpl.setNumStates(rules.getNumStates());
		CellImpl.setDefaultState(rules.getDefaultState());
		CellImpl.WORLD = this;
		
		//Initialize grid if no starter grid was provided
		for(int row = 0; row < height; row++){
			for(int col = 0; col < width; col++){
				if(startingGrid == null){
					grid[row][col] = new CellImpl(row, col, CellImpl.DEFAULT_STATE);
				}
				else{
					grid[row][col] = new CellImpl(row, col, startingGrid[row][col]);
				}
			}
		}
		
	}
	
	/**
	 * Get the state of an adjacent cell
	 * @param active The cell to get the adjacent cell of
	 * @param dir The direction to look in for the adjacent cell
	 * @return The state of the adjacent cell in direction dir
	 */
	protected int getAdjacentCellState(CellImpl active, Direction dir) {
		switch(dir){
			case N:
				return getLocationState(active.getRow()-1, active.getCol());
			case E:
				return getLocationState(active.getRow(), active.getCol()+1);
			case S:
				return getLocationState(active.getRow()+1, active.getCol());
			case W:
				return getLocationState(active.getRow(), active.getCol()-1);
			case NE:
				return getLocationState(active.getRow()-1, active.getCol()+1);
			case SE:
				return getLocationState(active.getRow()+1, active.getCol()+1);
			case SW:
				return getLocationState(active.getRow()+1, active.getCol()-1);
			case NW:
				return getLocationState(active.getRow()-1, active.getCol()-1);
		}
		return CellImpl.DEFAULT_STATE;
	}
	
	/**
	 * Gets the state of a cell at a given location
	 * This takes effect of locations that are past the border of the grid
	 * @param row The effective row of the cell to reference
	 * @param col The effective column of the cell to reference
	 * @return The state of a "cell" at the specified location, or default if
	 * 			the location is past the border of the grid
	 */
	public int getLocationState(int row, int col){
		//Check of the row is within the grid
		if(row < 0 || row >= height){
			//If the grid wraps around, get the cell on the other border
			if(toroidal){
				row = (row + height) % height;
			}
			//Otherwise, get the default state
			else{
				return CellImpl.DEFAULT_STATE;
			}
		}
		//Check of the row is within the grid
		if(col < 0 || col >= width){
			//If the grid wraps around, get the cell on the other border
			if(toroidal){
				col = (col + width) % width;
			}
			//Otherwise, get the default state
			else{
				return CellImpl.DEFAULT_STATE;
			}
		}
		//Return the state of the cell at the real location
		return grid[row][col].getState();
	}
	
	/**
	 * Sets the state of a cell at a given location
	 * @param state The state to set the cell to
	 * @param row The effective row of the cell to reference
	 * @param col The effective column of the cell to reference
	 */
	public void setStateAtRowCol(int state, int row, int col){
		//Check of the row is within the grid
		if(row < 0 || row >= height){
			//location is not valid
			return;
		}
		//Check of the row is within the grid
		if(col < 0 || col >= width){
			//location is not valid
			return;
		}
		grid[row][col].setState(state);
	}
	
	/**
	 * Gets the width (num columns) of the grid
	 * @return the width of the grid
	 */
	public int getWidth(){
		return width;
	}
	
	/**
	 * Gets the height (num rows) of the grid
	 * @return the height of the grid
	 */
	public int getHeight(){
		return height;
	}
	
	/**
	 * Gets the grid that contains the cells in the simulation
	 * @return the grid that represents the framework.world
	 */
	public CellImpl[][] getGrid(){
		return grid;
	}
	
	/**
	 * Gets the RuleSet that operates on a framework.world
	 * @return the RuleSet that defines the framework.world
	 */
	public RuleSet getRules(){
		return rules;
	}
	
	/**
	 * Gets the Visualization that displays the framework.world
	 * @return the Visualization that displays cells in the framework.world
	 */
	public Visualization getVisualization(){
		return visualizer;
	}

	/**
	 * Computes the next state of the framework.world in the simulation
	 * Computes the state of each cell given the current grid, and then
	 * updates all cells to the new state
	 */
	public void advanceWorldState(){
		//Create a new grid to hold the next state
		CellImpl[][] nextGrid = new CellImpl[height][width];
		
		//Gets the new state of each cell
		for(int i = 0; i < height; i++){
			for(int j = 0; j < width; j++){
				//Create a cell in the new grid with the new state
				CellImpl next = new CellImpl(i,j, CellImpl.DEFAULT_STATE);
				next.setState(rules.getNextState(grid[i][j]));
				nextGrid[i][j] = next;
			}
		}
		//Update the grid to point to the new grid
		grid = null;
		grid = nextGrid;
	}

	/**
	 * Gets the machine state from the framework.world
	 * @return the machine state of the framework.world
	 */
	public int getMachineState() {
		return machineState;
	}

	/**
	 * Sets the machine state of the framework.world
	 * @param newMachineState The new machine state of the framework.world
	 */
	public void setMachineState(int newMachineState) {
		machineState = newMachineState;
	}
	
}